package pdfsetter.impl.pdfbox.compiler;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSFloat;
import org.apache.pdfbox.cos.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdfwriter.ContentStreamWriter;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.util.PDFOperator;

import pdfsetter.core.compiler.DocumentCompiler;
import pdfsetter.core.schema.DocumentSchema;
import pdfsetter.core.schema.PageBuildSchema;
import pdfsetter.core.schema.PageInsertSchema;

/**
 * This is {@link DocumentCompiler} implementation using PDF Box. It is
 * currently unstable and under development.
 * 
 * @see DocumentCompiler
 * @author Tomasz Krakowiak
 * 
 */
public class PdfBoxDocumentCompiler implements DocumentCompiler {

	public static final double MM_PER_INCH = 25.4;
	public static final double DEFAULT_USER_UNIT = 1./72.;

	@Override
	public void compile(DocumentSchema documentSchema,
			List<File> sourceDocuments, File destination) {

		try {
			// Load source documents.
			List<PDDocument> sourcePDDocuments = new ArrayList<PDDocument>(
					sourceDocuments.size());
			for (File sourceFile : sourceDocuments) {
				sourcePDDocuments.add(PDDocument.loadNonSeq(sourceFile, null));
			}

			// Create target document.
			PDDocument targetPDDocument = new PDDocument();

			// Compile document.
			for (PageBuildSchema pageBuildSchema : documentSchema
					.getPageBuildSchemas()) {
				// Create compiled page object.
				PDPage compiledPage = new PDPage();

				compiledPage.setMediaBox(new PDRectangle(
						(float) (pageBuildSchema.getPageInfo().getWidth()
								/ MM_PER_INCH / DEFAULT_USER_UNIT),
						(float) (pageBuildSchema.getPageInfo().getHeight()
								/ MM_PER_INCH / DEFAULT_USER_UNIT)));

				// New page content.
				PDStream newContents = new PDStream(targetPDDocument);
				OutputStream pageOutputStream = newContents
						.createOutputStream();
				
				COSDictionary resources = new COSDictionary();
				PDResources resources2 = new PDResources(resources);
				compiledPage.setResources(resources2);

				// Paint source pages to new page content.
				for (PageInsertSchema pageInsertSchema : pageBuildSchema
						.getPageInsertSchemas()) {
					PDDocument sourceDocument = sourcePDDocuments
							.get(pageInsertSchema.getPageSelection()
									.getFileNumber());
					PDPage sourcePage = (PDPage) sourceDocument
							.getDocumentCatalog()
							.getAllPages()
							.get(pageInsertSchema.getPageSelection()
									.getPageNumber());
					
					String namePrefix = Integer.toString(pageInsertSchema.getPageSelection().getFileNumber()) + "_" + Integer.toString(pageInsertSchema.getPageSelection().getPageNumber()) + "_";
					Set<String> namespace = new HashSet<String>();
					
					PDResources sourceResources = sourcePage.findResources();
					if(sourceResources != null){
						for(Map.Entry<COSName, COSBase> rd : sourceResources.getCOSDictionary().entrySet()){
							if(rd.getValue() instanceof COSDictionary){
								COSDictionary dic = (COSDictionary) rd.getValue();
								COSDictionary dstdic = (COSDictionary) resources.getDictionaryObject(rd.getKey());
								if(dstdic == null){
									dstdic = new COSDictionary();
									resources.setItem(rd.getKey(), dstdic);
								}
								for(Map.Entry<COSName, COSBase> i : dic.entrySet()){
									namespace.add(i.getKey().getName());
									dstdic.setItem(COSName.getPDFName(namePrefix + i.getKey().getName()), i.getValue());
								}
								//dstdic.addAll(dic);
							}
						}
					}

					// We can ignore UserUnits of input pages. They doesn't matter after scaling.
					float sourceHeight = sourcePage.findMediaBox().getHeight();
					float sourceWidth = sourcePage.findMediaBox().getWidth();
					double rotation = -pageInsertSchema
							.getDestinationPageRectangle().getRotation();

					// TODO: Rotation

					// Calculate scaling
					float targetHeight = (float) (pageInsertSchema
							.getDestinationPageRectangle().getHeight()
							/ MM_PER_INCH / DEFAULT_USER_UNIT);
					float targetWidth = (float) (pageInsertSchema
							.getDestinationPageRectangle().getWidth()
							/ MM_PER_INCH / DEFAULT_USER_UNIT);
					float rWidth = (float) ((sourceHeight * Math.sin(rotation)) + (sourceWidth * Math
							.cos(rotation)));
					float rHeight = (float) ((sourceHeight * Math.cos(rotation)) + (sourceWidth * Math
							.sin(rotation)));

					float scale = Math.min(targetHeight / rHeight, targetWidth
							/ rWidth);

					ContentStreamWriter writer = new ContentStreamWriter(
							pageOutputStream);

					// Based on PDF Reference sixth edition chapter 4.3.3
					// Graphic state operators (page 218) and 4.2.3
					// Transformation Matrices (page 207)
					{
						// TODO: Rotation
						List<Object> tokens = new ArrayList<Object>();
						tokens.add(PDFOperator.getOperator("q")); // Push
																	// graphic
																	// state

						// Transformation are actually applied in reverse order.
						// This is the order we multiply transformation matrices.
						
						// Move to destination
						tokens.add(COSInteger.ONE);
						tokens.add(COSInteger.ZERO);
						tokens.add(COSInteger.ZERO);
						tokens.add(COSInteger.ONE);
						tokens.add(new COSFloat(
								(float) (pageInsertSchema
										.getDestinationPageRectangle()
										.getHorizontalShift()
										/ MM_PER_INCH / DEFAULT_USER_UNIT + targetWidth / 2)));
						tokens.add(new COSFloat(
								(float) (pageInsertSchema
										.getDestinationPageRectangle()
										.getVerticalShift()
										/ MM_PER_INCH / DEFAULT_USER_UNIT + targetHeight / 2)));
						tokens.add(PDFOperator.getOperator("cm"));

						// Rotate
						tokens.add(new COSFloat((float) (Math.cos(rotation))));
						tokens.add(new COSFloat((float) (-Math.sin(rotation))));
						tokens.add(new COSFloat((float) (Math.sin(rotation))));
						tokens.add(new COSFloat((float) (Math.cos(rotation))));
						tokens.add(COSInteger.ZERO);
						tokens.add(COSInteger.ZERO);
						tokens.add(PDFOperator.getOperator("cm"));

						// Scale
						tokens.add(new COSFloat(scale));
						tokens.add(COSInteger.ZERO);
						tokens.add(COSInteger.ZERO);
						tokens.add(new COSFloat(scale));
						tokens.add(COSInteger.ZERO);
						tokens.add(COSInteger.ZERO);
						tokens.add(PDFOperator.getOperator("cm"));

						// Move center to (0,0)
						tokens.add(COSInteger.ONE);
						tokens.add(COSInteger.ZERO);
						tokens.add(COSInteger.ZERO);
						tokens.add(COSInteger.ONE);
						tokens.add(new COSFloat(-sourceWidth / 2));
						tokens.add(new COSFloat(-sourceHeight / 2));
						tokens.add(PDFOperator.getOperator("cm"));
						writer.writeTokens(tokens);
					}

					// Copy page content.
					
					{
						PDFStreamParser parser = new PDFStreamParser(
								sourcePage.getContents());
						parser.parse();
						List<Object> tokens = parser.getTokens();
						for(int i = 0; i < tokens.size(); i++){
							if(tokens.get(i) instanceof COSName && namespace.contains(((COSName)tokens.get(i)).getName())){
								tokens.set(i, COSName.getPDFName(namePrefix + ((COSName)tokens.get(i)).getName()));
							}
						}
						writer.writeTokens(tokens);
					}
					
//					InputStream is = sourcePage.getContents()
//							.createInputStream();
//					byte[] buf = new byte[10240];
//					int amountRead = 0;
//					while ((amountRead = is.read(buf, 0, 10240)) > -1) {
//						pageOutputStream.write(buf, 0, amountRead);
//					}
//					is.close();

					writer.writeTokens(Arrays.asList(PDFOperator
							.getOperator("Q"))); // Pop graphic state
				}
				pageOutputStream.close();

				// Set compiled page content.
				compiledPage.setContents(newContents);

				// Add page to document.
				targetPDDocument.addPage(compiledPage);
			}

			// Save target file.
			targetPDDocument.save(new FileOutputStream(destination));
			targetPDDocument.close();
			
			for(PDDocument i : sourcePDDocuments){
				i.close();
			}

			// TODO: Checked exceptions
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(
					"IOException occured while compiling document.", e);
		} catch (COSVisitorException e) {
			throw new RuntimeException("Compiler internal exception.", e);
		}

		// TODO: Tests
	}

}
